在第 29 天,我新增了一個載入器(一個 <div>Loading ...</div>)來顯示頁面正在載入資料。
在 Angular 20 中非常簡單,因為它是 httpResource 內建的功能。在 Vue 3 中,我安裝了 vueuse 並使用 useFetch composable 來發起網路請求。
| Framework | Approach | 
|---|---|
| Vue 3 | vueuse's useFetch Composable | 
| SvelteKit | navigating and the error helper function | 
| Angular 20 | built-in in httpResource | 
安裝 vueuse/core
npm install --save-exact @vueuse/core
將 usePost composable 替換為 useFetch composable。
export const postsUrl = 'https://jsonplaceholder.typicode.com/posts'
export const usersUrl = 'https://jsonplaceholder.typicode.com/users'
<script setup lang="ts">
import PostCard from '@/components/PostCard.vue';
import { postsUrl } from '@/constants/apiEndpoints';
import type { Post } from '@/types/post';
import { useFetch } from '@vueuse/core';
const {
  data: posts,
  isFetching,
  error,
} = useFetch<Post[]>(postsUrl).json()
</script>
userFetch 接受一個 URL,該 URL 可以是字串或者是 ref/shallowRef。.json() 函式將回傳 JSON 格式的資料給 data。
當資料正在載入時,isFetching 為 true,載入完成則為 false。
error 回傳在網路請求中發生的任何錯誤。
<template>
  <div v-if="isFetching" class="text-center mb-10">Loading ...</div>
  <div v-if="error" class="text-center mb-10">{{ error }}</div>
  <div v-if="posts" class="flex flex-wrap flex-grow">
    <p class="ml-2 w-full">Number of posts: {{ posts.length }}</p>
    <PostCard v-for="post in posts" :key="post.id" :post="post" />
  </div>
</template>
當 isFetching 為 true 時,div 元素會顯示靜態文字 Loading...。當發生錯誤時,{{ error }} 顯示網路錯誤訊息。當端點成功回傳貼文時,v-for 指令會迭代陣列並渲染 PostCard 元件。
載入指示器和錯誤訊息顯示在 +layout.svelte 中。
<script lang="ts">
	import { page, navigating } from '$app/state';
	let { children } = $props();
</script>
{#if navigating.to}
	<div>Loading page...</div>
{:else if page.error}
	{page.error.message}
{:else}
	<div class="container">
		{@render children?.()}
	</div>
{/if}
當 navigation.to 不為 null 時,頁面正在導覽並載入資料。因此,div 元素會顯示靜態文字 Loading page...。
如果 page.error 是一個 Error 物件,則 page.error.message
顯示錯誤訊息。
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive all posts
export const load: PageServerLoad = async ({ fetch }) => {
	const postResponse = await fetch(`${BASE_URL}/posts`);
	if (!postResponse.ok) {
		error(404, {
			message: 'Failed to fetch posts'
		});
	}
	const posts = (await postResponse.json()) as Post[];
	return { posts };
};
load 函式會檢查回應是否不正常,並使用 error 輔助函式 (helper function) 丟出包含自訂訊息的 404 錯誤。
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { PostcardComponent } from '../post/postcard.component';
import { PostsService } from '../post/services/posts.service';
@Component({
  selector: 'app-home',
  imports: [PostcardComponent],
  template: `... inline template ...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
  postService = inject(PostsService);
  postsRes = this.postService.posts;
  posts = computed(() => (this.postsRes.hasValue() ? this.postsRes.value() : []));
  error = computed<string>(() =>
    this.postsRes.status() === 'error' ? 'Error loading the posts.' : '',
  );
}
當 postsRes 資源的狀態為錯誤時,error 計算信號會回傳自訂的錯誤訊息。否則,錯誤訊息為空字串。
@if (postsRes.isLoading()) {
  <div>Loading...</div>
} @else if (error()) {
  <div>Error: {{ error() }}</div>
}
@if (posts(); as allPosts) {
    <div class="flex flex-wrap flex-grow">
      <p class="ml-2 w-full">Number of posts: {{ allPosts.length }}</p>
      @for (post of allPosts; track post.id) {
        <app-postcard [post]="post" />
      }
    </div>
}
當 postsRes.isLoading() 為 true 時,div 元素會顯示靜態文字 Loading...。
當 error 計算信號 (computed signal) 評估為 true 時,會顯示錯誤訊息。
當 allPosts 已定義時,模板會顯示貼文數量並迭代陣列來渲染 PostComponent。
首先,useFetch 組合函式透過路徑參數取得貼文。這很簡單,因為此 URL 僅依賴路徑參數 (path param),且可從 useRoute 組合函式解構取得。
<script setup lang="ts">
import { postsUrl, usersUrl } from '@/constants/apiEndpoints'
import type { Post } from '@/types/post'
import type { User } from '@/types/user'
import { useFetch } from '@vueuse/core'
import { computed, shallowRef, watch } from 'vue'
import { useRoute } from 'vue-router'
const { params } = useRoute()
const url = `${postsUrl}/${params.id}`
const { 
    data: post, 
    isFetching: isFetchingPost, 
    error: errorPost 
} = useFetch<Post>(url).json()
</script>
當貼文取得後,使用 useFetch 組合函式根據貼文的使用者 ID 取得使用者。
使用者 URL 會隨使用者 ID 變動,因此它是個 shallowRef。
const userUrl = shallowRef('')
const {
  data: user,
  isFetching: isFetchingUser,
  error: errorUser,
} = useFetch<User>(userUrl, { refetch: true, immediate: false }).json()
refetch: true 表示當 userUrl 改變時會發出新的請求。此外,userUrl 初始為空字串,因此不希望它立即觸發。最終的 useFetchOptions 如下:
{
     refetch: true,
     immediate: true,
}
修改watcher 來追蹤貼文並以程式方式更新 userUrl 的 shallowRef。
watch(
  () => ({ ...post.value }),
  ({ userId = undefined }) => (userUrl.value = userId ? `${usersUrl}/${userId}` : ''),
)
當 userUrl 非空白時,useFetch 組合函式會自動根據使用者 ID 取得使用者。
const isFetching = computed(() => isFetchingPost.value || isFetchingUser.value)
新增 isFetching 計算參考 (computed ref) 以在貼文或使用者載入時顯示載入器。
const error = computed(() => {
  if (errorPost.value) {
    return errorPost instanceof Error ? errorPost.message : 'Error retrieving a post.'
  }
  if (errorUser.value) {
    return errorUser instanceof Error ? errorUser.message : 'Error retrieving a user.'
  }
  return ''
})
新增 error 計算參考 (computed ref) 以顯示任何錯誤訊息。
<template>
  <div v-if="isFetching" class="text-center my-10">Loading...</div>
  <div v-if="error" class="text-center my-10">{{ error }}</div>
  <div v-if="post && user" class="mb-10">
    <h1 class="text-3xl">{{ post.title }}</h1>
    <div class="text-gray-500 mb-10">by {{ user.name }}</div>
    <div class="mb-10">{{ post.body }}</div>
  </div>
</template>
當 isFetching 為 true 時顯示載入器,錯誤訊息非空白時顯示錯誤訊息。模板在兩者資料皆成功載入後顯示貼文與使用者名稱。
import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// retreive a post by an ID
export const load: PageServerLoad = async ({ params, fetch }): Promise<PostWitUser> => {
	const post = (await retrieveResource(fetch, `posts/${+params.id}`, 'Post')) as Post;
	const user = (await retrieveResource(fetch, `users/${post.userId}`, 'User')) as User;
	return {
		post,
		user,
	};
};
retrieveResource 輔助函式 (helper function) 使用原生的 fetch 函式根據貼文 ID 取得貼文。當貼文成功取得後,此輔助函式使用貼文的使用者 ID 取得使用者。接著,load 函式會將貼文和使用者一併回傳給 +page.svelte。
type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
async function retrieveResource(fetch: FetchFunction, subPath: string, itemName: string) {
	const url = `${BASE_URL}/${subPath}`;
	const response = await fetch(url);
	if (!response.ok) {
		error(404, {
			message: `Failed to fetch ${itemName}`
		});
	}
	const item = await response.json();
	if (!item) {
		error(404, {
			message: `${itemName} does not exist`
		});
	}
	return new Promise((resolve) => {
		setTimeout(() => resolve(item), 1000);
	});
}
retrieveResource 會抓取該項目並檢查回應,當回應不正常時,會丟出帶有自訂訊息 Failed to fetch ${itemName} 的 404 錯誤。await response.json() 會將 Promise 解析成物件,若該物件為 undefined,也會丟出帶有自訂訊息 ${itemName} does not exist 的 404 錯誤。
return new Promise((resolve) => {
	setTimeout(() => resolve(item), 1000);
});
該 Promise 會延遲一秒以模擬載入行為。
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
import { UserService } from './services/user.service';
import { Post } from './types/post.type';
@Component({
  selector: 'app-post',
  styles: `
    @reference "../../styles.css";
    :host {
      @apply flex m-2  gap-2 items-center w-1/4 flex-grow rounded overflow-hidden w-full;
    }
  `,
  template: `... inline template ...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PostComponent {
  readonly userService = inject(UserService);
  post = input<Post>();
  userRef = this.userService.createUserResource(this.post);
  user = computed(() => (this.userRef.hasValue() ? this.userRef.value() : undefined));
  error = computed<string>(() =>
    this.userRef.status() === 'error' ? 'Error loading the post.' : '',
  );
}
@let myUser = user();
@let myPost = post();
@if (userRef.isLoading()) {
  <div>Loading...</div>
} @else if (error()) {
  <div>Error: {{ error() }}</div>
} @else if (myPost && myUser) {
  <div class="mb-10">
    <h1 class="text-3xl">{{ myPost.title }}</h1>
    <div class="text-gray-500 mb-10">by {{ myUser.name }}</div>
    <div class="mb-10">{{ myPost.body }}</div>
  </div>
} @else {
  <div>Post not found</div>
}
當 userRef.isLoading() 為 true 時,div 元素會顯示靜態文字 Loading...。
當 error 計算信號評估為 true 時,會顯示錯誤訊息。
最後的 elseif 顯示貼文標題、貼文內容和使用者名稱。
我們已成功在每個頁面實作簡易的載入器和錯誤指示器。
我以為會有第30天😆
我從第 0 天開始, 我還沒有完成,我想在我的最後一個例子中添加分頁。
嗯嗯,我有發現你有30篇文章了
但想說要在第30天恭喜你😂
反正已經30篇了,現在不急每天發文了!